package bsh;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Map.Entry;

import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import bsh.compilation.DebugOptions;
import bsh.exception.CompilationAssertionFailed;
import bsh.meta.FieldDescriptor;
import bsh.meta.JavaClassWrapper;
import bsh.meta.MethodDescriptor;
import bsh.meta.MethodScope;
import bsh.meta.Scope;
import bsh.meta.ScopeImpl;
import bsh.meta.ScopeType;
import bsh.meta.TypeWrapper;

/**
 * Mantem um estado sobre o que esta sendo construido
 * 
 * @author takeshi
 */
public class CompilationState {

    private final ScopeImpl global;

    private final LinkedList<TypeWrapper> stack = new LinkedList<TypeWrapper>();

    private final LinkedList<ScopeImpl> scopes = new LinkedList<ScopeImpl>();

    private final Map<String, FieldDescriptor> fields = new HashMap<String, FieldDescriptor>();

    private int lastLine = -1;

    private final CompilationUnit unit;

    /**
     * @param unit {@link CompilationUnit}
     */
    public CompilationState(CompilationUnit unit) {
        this.global = unit.compilationUnitScope();
        this.unit = unit;
        this.scopes.add(this.global);
    }

    /**
     * Procura o scopo com o tipo passado ou devolve null
     * 
     * @param type {@link ScopeType}
     * @return {@link Scope}
     */
    public Scope findEnclosingScopeWithType(ScopeType type) {
        Scope[] array = this.scopes.toArray(new ScopeImpl[this.scopes.size()]);
        for (int i = array.length - 1; i >= 0; i--) {
            final Scope current = array[i];
            if (current.getType() == type) {
                return current;
            }
        }
        return null;
    }

    /**
     * Procura o metodo
     * 
     * @return {@link MethodScope}
     */
    public MethodScope findEnclosingMethod() {
        return (MethodScope) this.findEnclosingScopeWithType(ScopeType.METHOD);
    }

    /**
     * Cria um novo escopo. O tipo de scopo nao pode ser {@link ScopeType#GLOBAL} e nem {@link ScopeType#METHOD}
     * 
     * @param type {@link ScopeType}
     * @return {@link Scope}
     */
    public Scope newScope(ScopeType type) {
        if (type == ScopeType.GLOBAL) {
            throw new CompilationAssertionFailed("Illegal attempt to create a second global scope!");
        }
        if (type == ScopeType.METHOD) {
            throw new CompilationAssertionFailed("cannot create scope method without method descriptor");
        }
        ScopeImpl s = new ScopeImpl(this.scopes.getLast(), type);
        this.scopes.add(s);
        return s;
    }

    /**
     * Cria um novo escopo de Metodo
     * 
     * @param desc {@link MethodDescriptor}
     * @param declaringScope the scope that declared the method
     * @return {@link MethodScope}
     */
    public MethodScope newMethodScope(MethodDescriptor desc, Scope declaringScope) {
        MethodScope s = new MethodScope(this.scopes.getLast(), desc);
        for (Entry<String, TypeWrapper> entry : declaringScope.getImports().entrySet()) {
            s.addTypeAlias(entry.getKey(), entry.getValue());
        }
        this.scopes.add(s);
        return s;
    }

    /**
     * Faz um pop do ultimo escopo. O escopo atual nao pode ser {@link ScopeType#GLOBAL}
     * 
     * @return Scope
     */
    public Scope popScope() {
        if (this.scopes.size() <= 1) {
            throw new CompilationAssertionFailed("scope depth: " + this.scopes.size());
        }
        return this.scopes.removeLast();
    }

    /**
     * Olha o ultimo elemento do stack
     * 
     * @return {@link TypeWrapper}
     */
    public TypeWrapper peekStack() {
        this.checkEmptyStack();
        return this.stack.getLast();
    }

    /**
     * @return {@link TypeWrapper}
     */
    public TypeWrapper popStack() {
        this.checkEmptyStack();
        return this.stack.removeLast();
    }

    /**
     * Verifica que o topo do stack e o tipo passado sao compativeis
     * 
     * @param type {@link TypeWrapper}
     */
    public void assertStackTopType(TypeWrapper type) {
        this.checkEmptyStack();
        if (!type.isSuperTypeOf(this.stack.getLast())) {
            throw new CompilationAssertionFailed("required type: " + type + " found on top of stack: " + this.stack.getLast());
        }
    }

    /**
     * Empurra uma classe para o stack
     * 
     * @param javaClass Class
     */
    public void pushStack(Class<?> javaClass) {
        this.pushStack(JavaClassWrapper.wrapperFor(javaClass));
    }

    /**
     * Empurra uma classe para o stack
     * 
     * @param type {@link TypeWrapper}
     */
    public void pushStack(TypeWrapper type) {
        if (type == null) {
            throw new CompilationAssertionFailed("cannot push null or void");
        }
        this.stack.add(type);
    }

    /**
     * Verifica que o stack nao esta vazio
     */
    private void checkEmptyStack() {
        if (this.stack.isEmpty()) {
            throw new CompilationAssertionFailed("empty stack");
        }
    }

    /**
     * Verifica que o stack tem pelo menos min elementos
     * 
     * @param min int
     */
    public void assertMinimumStackSize(int min) {
        if (this.stack.size() < min) {
            throw new CompilationAssertionFailed("stack size: " + this.stack.size() + " required at least: " + min);
        }
    }

    /**
     * Verifica que o stack tem exatamente size elementos
     * 
     * @param size int
     */
    public void assertStackSize(int size) {
        if (this.stack.size() != size) {
            throw new CompilationAssertionFailed("stack size: " + this.stack.size() + " required: " + size + ". Content: " + this.stack);
        }

    }

    /**
     * Verifica que o stack esta vazio
     */
    public void assertEmptyStack() {
        if (!this.stack.isEmpty()) {
            throw new CompilationAssertionFailed("stack state: " + this.stack);
        }
    }

    /**
     * Adiciona informacoes de numero de linha
     * 
     * @param visitor visitor
     * @param line numero da linha
     */
    public void addLineNumber(MethodVisitor visitor, int line) {
        if (this.unit.options().debugOptions().contains(DebugOptions.LINE_NUMBERS)) {
            if (this.lastLine < line) {
                Label label = new Label();
                visitor.visitLabel(label);
                visitor.visitLineNumber(line, label);
                this.lastLine = line;
            }
        }
    }

    /**
     * @param toPop int
     */
    public void popStack(int toPop) {
        for (int i = 0; i < toPop; i++) {
            this.popStack();
        }
    }

    /**
     * @return a altura do stack
     */
    public int getStackSize() {
        return this.stack.size();
    }

    /**
     * @param field {@link FieldDescriptor}
     */
    public void addField(FieldDescriptor field) {
        this.fields.put(field.fieldName(), field);
    }

    /**
     * @return o escopo atual
     */
    public Scope currentScope() {
        return this.scopes.getLast();
    }

    /**
     * Empurra 'null' no stack
     */
    public void pushNull() {
        this.stack.add(JavaClassWrapper.NULL_TYPE_WRAPPER);
    }

    /**
     * Remove o ultimo elemento do stack
     * 
     * @param visitor {@link MethodVisitor}
     * @return {@link TypeWrapper} o tipo tirado
     */
    public TypeWrapper popStack(MethodVisitor visitor) {
        TypeWrapper wrapper = this.popStack();
        if (wrapper.numberOfSlots() == 2) {
            visitor.visitInsn(Opcodes.POP2);
        } else {
            visitor.visitInsn(Opcodes.POP);
        }
        return wrapper;
    }

    /**
     * @return the unit
     */
    public CompilationUnit getUnit() {
        return this.unit;
    }

    /**
     * @param string the message
     */
    public void assertEmptyStack(String string) {
        if (!this.stack.isEmpty()) {
            throw new CompilationAssertionFailed(string + " || stack state: " + this.stack);
        }
    }

}
